#!/usr/bin/env python
#
# Copyright 2008 Richard J. Bell. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

""" Wiki classes for geo wiki (great-loop) site. """

# Python Imports
import re
import urllib
import urlparse

# Google App Engine Imports
from google.appengine.api import memcache
from google.appengine.ext import db

# Application Imports
from model import WikiUser
from app.userhandlers import UserSignIn
from app.wikigeo import WikiGeometry
    
class WikiPageRevision( db.Model ):
    """ db model for a wiki page revision
        Holds copies of previous versions of wiki pages
        Attributes should generally match WikiPage
    """
    wiki_title = db.StringProperty( )   # unique wikiword 
    title = db.StringProperty( )        # conventional title
    content = db.TextProperty( )
    author = db.ReferenceProperty( WikiUser )
    version = db.IntegerProperty( )
    date = db.DateTimeProperty( )
    tags = db.ListProperty(unicode,default=None)
    location = db.GeoPtProperty( default=None )
    type = db.StringProperty()
    coordinates = db.ListProperty(db.GeoPt, default=None)
    bboxEast = db.FloatProperty()
    bboxWest = db.FloatProperty()
    bboxSouth = db.FloatProperty()
    bboxNorth = db.FloatProperty()
    geohash = db.StringProperty()
    
class WikiPage( db.Model ):
    """ Wiki page class.
      Holds the datastore definition of a wiki page.
      
      Handles all datastore operations. To create OR edit a page, just 
      create a Page instance and call save().

      These methods use memcache to cache recent wiki pages.  The wiki_title
      is the cach key.  Cache entries contain a WikiPage object.
    """
    # unique wikiword 
    wiki_title = db.StringProperty( required = True, multiline = False )
    # conventional title
    title = db.StringProperty( required = True, multiline = False )
    content = db.TextProperty( required = True )
    author = db.ReferenceProperty( WikiUser )
    version = db.IntegerProperty( required = True )
    date = db.DateTimeProperty( auto_now_add = True )
    tags = db.ListProperty(unicode,default=None)
    # geo location
    location = db.GeoPtProperty( default=None )
    type = db.StringProperty()
    coordinates = db.ListProperty(db.GeoPt, default=None)
    # bounding box to allow us to do queries near here
    # only required to support coordinate lists
    bboxEast = db.FloatProperty()
    bboxWest = db.FloatProperty()
    bboxSouth = db.FloatProperty()
    bboxNorth = db.FloatProperty()
    # supports get locations near query
    geohash = db.StringProperty()

    @staticmethod
    def save( wiki_title, title, content ):
        """ Save a wiki page.
            If the page exists, the old one is saved as a revision and the
            version number incrementd.
            The content and title of the wiki page wiki_title are saved to
            the data store.
            
            Parameters:
                wiki_title, str: the wiki_title for the page
                title, str: The page title text.  May have imbedded html
                content, str: The content of the page.
            Returns: none
        """
        # Get the current app_server_user and wiki_user
        current_user, wiki_user = UserSignIn.require_signin()
        # Find the entry, if it exists
        page = WikiPage.gql('WHERE wiki_title = :1', wiki_title).get()
        if page:    # the page exists, so this is a new version
            # save the current page as a revision and delete the old one
            revision = WikiPageRevision( wiki_title = page.wiki_title,
                                         title = page.title,
                                         content = page.content,
                                         author = page.author,
                                         version = page.version,
                                         date = page.date,
                                         tags = page.tags,
                                         location = page.location,
                                         type = page.type,
                                         coordinates = page.coordinates,
                                         bboxEast = page.bboxEast,
                                         bboxWest = page.bboxWest,
                                         bboxSouth = page.bboxSouth,
                                         bboxNorth = page.bboxNorth,
                                         geohash = page.geohash
                                         )
            revision.put()
            page.delete()
            version = revision.version + 1
        else:
            version = 1
        # save this page
        # todo: here we need to add code to initialize the page's geo crud
        page = WikiPage( wiki_title = wiki_title,
                         title = title,
                         content = content,
                         author = wiki_user,
                         version = version
                         )
        page.location = db.GeoPt( 42.37190698797091, -71.44295990467072 )
        page.put()
        memcache.set( wiki_title, page, 600 )

    @staticmethod
    def exists( wiki_title ):
        """ Returns true if the page exists in the datastore.
        
        Parameters: wiki_title, str: The page's wiki_title
        Returns: the page if the page exists, false otherwise
        """
        return WikiPage.gql('WHERE wiki_title = :1', wiki_title).get()

    @staticmethod
    def load_db( wiki_title ):
        """ Load the page from the datastore.
        
          If the page exists return it.  If not, a new Page is returned.

          Parameters: page_title, str: The page title
          Returns: WikiPage object
        """
        page = WikiPage.gql('WHERE wiki_title = :1', wiki_title).get()
        if not page:
            page = WikiPage( 
                wiki_title = wiki_title,
                title = "Replace title for page %s" % wiki_title,
                content = "Content for page %s" % wiki_title,
                author = None,
                version = 1
                )
        return page

    @staticmethod
    def load( wiki_title ):
        """ Load the page.
        
          Load the page from memcache if possible.  Otherwise, load the page
          from the datastore.  If the page exists the most recent version
          version is returned.  If not, a blank Page is returned.          
          
          Parameters: page_title, str: The page title
          Returns: WikiPage
        """
        page = memcache.get( wiki_title )
        if page is None:
            page = WikiPage.load_db( wiki_title )
            memcache.set( wiki_title, page, 600 )
        return page

    @staticmethod
    def listall( ):
        """ List all the wiki pages.
        Returns: [collection of all WikiPage(s)] for all wiki pages 
                 sorted by title.
        """
        query = WikiPage.all()
        query.order('title')
        wiki_list = []
        for wiki in query:
            wiki_list.append( wiki )
            ## Retrieve the current version
            #current_version = WikiRevision.gql('WHERE wiki_page =  :1 '
                                               #'ORDER BY version_number DESC', wiki).get()
            ## Define the body, version number, author email, author nickname 
            ## and revision date
            #version = str( current_version.version_number )
            #author_email = urllib.quote(current_version.author.user.email())
            #author_nickname = current_version.author.user.nickname()
            #version_date = current_version.created
            #wiki_list.append({ 'title': wiki.title,
                               #'version': version,
                               #'email': author_email,
                               #'nickname': author_nickname,
                               #'date': version_date })
        return wiki_list

    @staticmethod
    def wikify( wiki_body ):
        """ Apply wiki transforms to wiki_body and return for display.
        
        Parameters: str, wiki_body - the non-wikified wiki contents
        Returns: str: Contents wikified.
        """
        transforms = [
            AutoLink(),
            WikiWords(),
            HideReferers(),
        ]
        for transform in transforms:
            wiki_body = transform.run(wiki_body)
        return wiki_body

class Transform(object):
    """ Abstraction for a regular expression transform.
      Transform subclasses have two properties:
      
      regexp: the regular expression defining what will be replaced
      replace(MatchObject): returns a string replacement for a regexp 
                            match
                            
      We iterate over all matches for that regular expression, calling 
        replace() on the match to determine what text should replace the 
        matched text.
        
      The Transform class is more expressive than regular expression 
        replacement because the replace() method can execute arbitrary code 
        to, e.g., look up a WikiWord to see if the page exists before 
        determining if the WikiWord should be a link.
    """
    def run(self, content):
        """ Runs this transform over the given content.
        Parameters: content: str, The data to apply a transformation to.
        Returns: str, Data after transformation is applied.
        """
        parts = []
        offset = 0
        for match in self.regexp.finditer(content):
            parts.append(content[offset:match.start(0)])
            parts.append(self.replace(match))
            offset = match.end(0)
        parts.append(content[offset:])
        return ''.join(parts)

class AutoLink(Transform):
    """ Generate links for URL text. """
    def __init__(self):
        self.regexp = re.compile(
            r'([^"])\b((http|https)://[^ \t\n\r<>\(\)&"]+' \
            r'[^ \t\n\r<>\(\)&"\.])')
    def replace(self, match):
        url = match.group(2)
        return match.group(1) + \
               '<a class="autourl" href="%s">%s</a>' % (url, url)

class WikiWords(Transform):
    """ Translates WikiWords to links.
      Look up all the WikiWords (ie. camel case words) and link those that
      have current wiki pages.
  """
    def __init__(self):
        self.regexp = re.compile(r'[A-Z][a-z]+([A-Z][a-z]+)+')
    def replace(self, match):
        wikiword = match.group(0)
        if WikiPage.exists(wikiword):
            return '<a class="wikiword" href="/%s">%s</a>' % (wikiword, wikiword)
        else:
            return wikiword

class AutoLink(Transform):
    """ Generate links for URL text, ex. http://www.google.com. """
    def __init__(self):
        self.regexp = re.compile(
            r'([^"])\b((http|https)://[^ \t\n\r<>\(\)&"]+' \
            r'[^ \t\n\r<>\(\)&"\.])')
    def replace(self, match):
        url = match.group(2)
        return match.group(1) + \
               '<a class="autourl" href="%s">%s</a>' % (url, url)

class HideReferers(Transform):
    """A transform that hides referers for external hyperlinks."""
    def __init__(self):
        self.regexp = re.compile(r'href="(http[^"]+)"')
    def replace(self, match):
        url = match.group(1)
        scheme, host, path, parameters, query, fragment = urlparse.urlparse(url)
        url = 'http://www.google.com/url?sa=D&amp;q=' + urllib.quote(url)
        return 'href="%s"' % (url,)

class WikiComment(db.Model):
    """ A wiki comment. """
    wiki_page = db.ReferenceProperty(WikiPage)
    comment = db.TextProperty(required=True)
    author = db.ReferenceProperty(WikiUser)
    created = db.DateTimeProperty(auto_now_add=True)
